



package wviz.gui.graph;

import prefuse.Visualization;
import prefuse.data.Tuple;
import prefuse.data.event.TupleSetListener;
import prefuse.data.search.PrefixSearchTupleSet;
import prefuse.data.search.SearchTupleSet;
import prefuse.data.tuple.TupleSet;
import prefuse.util.ColorLib;

import javax.swing.*;
import javax.swing.undo.UndoableEdit;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.Document;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

/**
 * Search panel similar to Prefuse JSearchPanel which is used for handeling search in Prefuse visualisation graph and
 * underlaying data container.
 * This class update listeners too.
 * <p/>
 * NOTE:
 * Almost  all of the this class code is copied from JSearchPanel
 * <p/>
 *
 * @author Mohammad Asgari
 * @version 1.0
 * @date 19-Dec-2008
 *
 * @see <link>http://prefuse.org/doc//api/prefuse/util/ui/JSearchPanel.html</link>
 */
public class SearchPanel extends JPanel implements DocumentListener, ActionListener {
    //a search box container for query field and cancel button
    private Box m_sbox = new Box(BoxLayout.X_AXIS);
    //search text field contains user entered query
    private JTextField m_queryF = new JTextField(15);
    //find result items
    private JLabel m_resultL = new JLabel("          ");
    //label for search box
    private JLabel m_searchL = new JLabel("Search >> ");

    private Color m_cancelColor = ColorLib.getColor(255, 75, 75);

    private boolean m_includeHitCount = true;
    private boolean m_monitorKeys = false;
    private boolean m_autoIndex = true;

    private boolean m_showBorder = true;
    private boolean m_showCancel = true;

    private Object m_lock;

    private String[] m_fields;

    //searcher used for searching in Prefuse visualised graph
    private SearchTupleSet m_searcher;

    /**
     * Create a new JSearchPanel.
     *
     * @param fields            the data fields being searched
     * @param autoIndex         indicates if items should be automatically
 *                          indexed and unindexed as their membership in the source group
 *                          changes.
     * @param monitorKeystrokes indicates if each keystroke event should result
*                          in a new search being issued (true) or if searches should only be
     */
    public SearchPanel(
            String[] fields, boolean autoIndex, boolean monitorKeystrokes) {
        m_fields = fields;
        m_autoIndex = autoIndex;
        m_monitorKeys = monitorKeystrokes;
        initUI();
    }

    /**
     * Set visual searcher object which will search in Prefuse visualised graph.
     * Visual searcher is a subclass of SearchTupleSet which can be set by
     * <pre>
     *  Visualization vis = ...
     *  SearchTupleSet searcher = new PrefixSearchTupleSet();
     *  vis.addFocusGroup(searchGroup, searcher);
     * </pre>
     *
     * @param display the display object
     */
    public void update(BaseDisplay display) {
        Visualization vis = display.getVisualization();
        m_lock = vis;
        TupleSet source = vis.getGroup(display.getDataGroup());
        TupleSet search = vis.getGroup(display.getSearchGroup());

        if (search != null) {
            if (search instanceof SearchTupleSet) {
                m_searcher = (SearchTupleSet) search;
            } else {
                throw new IllegalStateException(
                        "Search focus set not instance of SearchTupleSet!");
            }
        } else {
            m_searcher = new PrefixSearchTupleSet();
            vis.addFocusGroup(display.getSearchGroup(), m_searcher);
        }

        if (m_autoIndex && source != null) {
            // index everything already there
            for (String m_field : m_fields)
                m_searcher.index(source.tuples(), m_field);

            // add a listener to dynamically build search index
            source.addTupleSetListener(new TupleSetListener() {
                public void tupleSetChanged(TupleSet tset,
                                            Tuple[] add, Tuple[] rem) {
                    if (add != null) {
                        for (Tuple anAdd : add) {
                            for (String m_field : m_fields)
                                m_searcher.index(anAdd, m_field);
                        }
                    }
                    if (rem != null && m_searcher.isUnindexSupported()) {
                        for (Tuple aRem : rem) {
                            for (String m_field : m_fields)
                                m_searcher.unindex(aRem, m_field);
                        }
                    }
                }
            });
        }
        //force to highlight search results for previously entered query in updated display
        setQuery(getQuery());
    }

    protected void initUI() {
        this.removeAll();
        this.setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
        m_queryF.setToolTipText("Press enter to search.");
        m_queryF.addActionListener(this);
        m_queryF.setToolTipText("Use space for multiple word highlighting");
        if (m_monitorKeys) {
            m_queryF.getDocument().removeDocumentListener(this);//remove if already added (to prevent multiple event fireing)
            m_queryF.getDocument().addDocumentListener(this);
        }
        m_queryF.setMaximumSize(new Dimension(400, 100));
        m_queryF.setPreferredSize(new Dimension(200, 20));
        m_queryF.setBorder(null);

        m_sbox.removeAll();
        m_sbox.add(Box.createHorizontalStrut(3));
        m_sbox.add(m_queryF);
        m_sbox.add(Box.createHorizontalStrut(3));
        if (m_showCancel) {
            m_sbox.add(new CancelButton());
            m_sbox.add(Box.createHorizontalStrut(3));
        }
        if (m_showBorder)
            m_sbox.setBorder(BorderFactory.createLineBorder(getForeground()));
        else
            m_sbox.setBorder(null);
        m_sbox.setMaximumSize(new Dimension(400, 100));
        m_sbox.setPreferredSize(new Dimension(171, 20));

        Box b = new Box(BoxLayout.X_AXIS);
        if (m_includeHitCount) {
            b.add(m_resultL);
            b.add(Box.createHorizontalStrut(10));
            //b.add(Box.createHorizontalGlue());
        }
        b.add(m_searchL);
        b.add(Box.createHorizontalStrut(3));
        b.add(m_sbox);

        this.add(b);
    }

    // ------------------------------------------------------------------------

    /**
     * Request the keyboard focus for this component.
     */
    public void requestFocus() {
        this.m_queryF.requestFocus();
    }

    /**
     * Set the lock, an object to synchronize on while issuing queries.
     *
     * @param lock the synchronization lock
     */
    public void setLock(Object lock) {
        m_lock = lock;
    }

    /**
     * Update search text box field (m_queryF) for listening user typed characters
     * @param monitorKeywords
     */
    public void setMonitorKeywords(boolean monitorKeywords) {
        m_monitorKeys = monitorKeywords;
        if (m_monitorKeys) {
            m_queryF.getDocument().removeDocumentListener(this);//remove if already added (to prevent multiple event fireing)
            m_queryF.getDocument().addDocumentListener(this);
        }
    }
    /**
     * Indicates if the component should show the number of search results.
     *
     * @param b true to show the result count, false to hide it
     */
    public void setShowResultCount(boolean b) {
        this.m_includeHitCount = b;
        initUI();
        validate();
    }

    /**
     * Indicates if the component should show a border around the text field.
     *
     * @param b true to show the text field border, false to hide it
     */
    public void setShowBorder(boolean b) {
        m_showBorder = b;
        initUI();
        validate();
    }

    /**
     * Indicates if the component should show the cancel query button.
     *
     * @param b true to show the cancel query button, false to hide it
     */
    public void setShowCancel(boolean b) {
        m_showCancel = b;
        initUI();
        validate();
    }

    /**
     * Update the search results based on the current query.
     */
    @SuppressWarnings({"SynchronizeOnNonFinalField"})
    protected void searchUpdate() {
        String query = m_queryF.getText();
        firePropertyChange("query", null, query);
        synchronized (m_lock) {
            m_searcher.search(query);
            if (m_searcher.getQuery().length() == 0)
                m_resultL.setText(null);
            else {
                int r = m_searcher.getTupleCount();
                m_resultL.setText(r + " match" + (r == 1 ? "" : "es"));
            }
        }
    }

    /**
     * Set the query string in the text field.
     *
     * @param query the query string to use
     */
    public void setQuery(String query) {
        Document d = m_queryF.getDocument();
        d.removeDocumentListener(this);
        //String oldValue = getQuery();
        m_queryF.setText(query);
        if (m_monitorKeys)
            d.addDocumentListener(this);
        searchUpdate();
    }

    /**
     * Get the query string in the text field.
     *
     * @return the current query string
     */
    public String getQuery() {
        return m_queryF.getText();
    }

    /**
     * Set the fill color of the cancel 'x' button that appears
     * when the button has the mouse pointer over it.
     *
     * @param c the cancel color
     */
    public void setCancelColor(Color c) {
        m_cancelColor = c;
    }

    /**
     * @see java.awt.Component#setBackground(java.awt.Color)
     */
    public void setBackground(Color bg) {
        super.setBackground(bg);
        if (m_queryF != null) m_queryF.setBackground(bg);
        if (m_resultL != null) m_resultL.setBackground(bg);
        if (m_searchL != null) m_searchL.setBackground(bg);
    }

    /**
     * @see java.awt.Component#setForeground(java.awt.Color)
     */
    public void setForeground(Color fg) {
        super.setForeground(fg);
        if (m_queryF != null) {
            m_queryF.setForeground(fg);
            m_queryF.setCaretColor(fg);
        }
        if (m_resultL != null) m_resultL.setForeground(fg);
        if (m_searchL != null) m_searchL.setForeground(fg);
        if (m_sbox != null && m_showBorder)
            m_sbox.setBorder(BorderFactory.createLineBorder(fg));
    }

    /**
     * @see javax.swing.JComponent#setOpaque(boolean)
     */
    public void setOpaque(boolean opaque) {
        super.setOpaque(opaque);
        if (m_queryF != null) {
            m_queryF.setOpaque(opaque);
        }
        if (m_resultL != null) m_resultL.setOpaque(opaque);
        if (m_searchL != null) m_searchL.setOpaque(opaque);
    }

    /**
     * @see java.awt.Component#setFont(java.awt.Font)
     */
    public void setFont(Font f) {
        super.setFont(f);
        if (m_queryF != null) m_queryF.setFont(f);
        if (m_resultL != null) m_resultL.setFont(f);
        if (m_searchL != null) m_searchL.setFont(f);
    }

    /**
     * Set the label text used on this component.
     *
     * @param text the label text, use null to show no label
     */
    public void setLabelText(String text) {
        m_searchL.setText(text);
    }


    /**
     * @see javax.swing.event.DocumentListener#changedUpdate(javax.swing.event.DocumentEvent)
     */
    public void changedUpdate(DocumentEvent e) {
        searchUpdate();
    }

    /**
     * @see javax.swing.event.DocumentListener#insertUpdate(javax.swing.event.DocumentEvent)
     */
    public void insertUpdate(DocumentEvent e) {
        searchUpdate();
    }

    /**
     * @see javax.swing.event.DocumentListener#removeUpdate(javax.swing.event.DocumentEvent)
     */
    public void removeUpdate(DocumentEvent e) {
        searchUpdate();
    }

    /**
     * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
     */
    public void actionPerformed(ActionEvent e) {
        Object src = e.getSource();
        if (src == m_queryF) {
            searchUpdate();
        }
    }

    /**
     * A button depicted as an "X" that allows users to cancel the current query
     * and clear the query field.
     */
    public class CancelButton extends JComponent implements MouseListener {

        private boolean hover = false;
        private int[] outline = new int[]{
                0, 0, 2, 0, 4, 2, 5, 2, 7, 0, 9, 0, 9, 2, 7, 4, 7, 5, 9, 7, 9, 9,
                7, 9, 5, 7, 4, 7, 2, 9, 0, 9, 0, 7, 2, 5, 2, 4, 0, 2, 0, 0
        };
        private int[] fill = new int[]{
                1, 1, 8, 8, 1, 2, 7, 8, 2, 1, 8, 7, 7, 1, 1, 7, 8, 2, 2, 8, 1, 8, 8, 1
        };

        public CancelButton() {
            // set button size
            Dimension d = new Dimension(10, 10);
            this.setPreferredSize(d);
            this.setMinimumSize(d);
            this.setMaximumSize(d);

            // prevent the widget from getting the keyboard focus
            this.setFocusable(false);

            // add callbacks
            this.addMouseListener(this);

            setToolTipText("Clear search query");
        }

        public void paintComponent(Graphics g) {
            if (hover) { // draw fill
                g.setColor(m_cancelColor);
                for (int i = 0; i + 3 < fill.length; i += 4) {
                    g.drawLine(fill[i], fill[i + 1], fill[i + 2], fill[i + 3]);
                }
            }
            g.setColor(SearchPanel.this.getForeground());
            for (int i = 0; i + 3 < outline.length; i += 2) {
                g.drawLine(outline[i], outline[i + 1],
                        outline[i + 2], outline[i + 3]);
            }
        }

        public void mouseClicked(MouseEvent arg0) {
            setQuery(null);
        }

        public void mousePressed(MouseEvent arg0) {
        }

        public void mouseReleased(MouseEvent arg0) {
        }

        public void mouseEntered(MouseEvent arg0) {
            hover = true;
            repaint();
        }

        public void mouseExited(MouseEvent arg0) {
            hover = false;
            repaint();
        }

    } // end of class CancelButton
}
